// ValidationAspects
// Written by Mike Saunders
// Released under Microsoft Public License (Ms-PL)
using System.Linq;
using System.Reflection;
using NUnit.Framework;
using ValidationAspects;
using ValidationAspects.Exceptions;
using ValidationAspects.Factories;
using ValidationAspects.Sdk;

namespace TestValidationAspects.Factories
{
	[TestFixture]
	public class TestValidateObject
	{
		[SetUp]
		public void SetUp()
		{
			TestHelpers.ResetFramework();
		}

		[TestFixtureTearDown]
		public void TestFixtureTearDown()
		{
			TestHelpers.ResetFramework();
		}

		[Test]
		public void Test_Invoke_for_Type_with_no_registered_factories()
		{
			new ValidateObject().Create<Dummy>().Invoke(new Dummy(), new MockValidationContext());
		}

		[Test]
		public void Test_Validate_a_null_value_skips_validation()
		{
			IValidator<Dummy> validator = new ValidateObject().Create<Dummy>();
			validator.Invoke(null, new MockValidationContext());
		}

		[Test]
		public void Test_Validate_a_null_nullable_value_skips_validation()
		{
			IValidator<int?> validator = new ValidateObject().Create<int?>();
			validator.Invoke(null, new MockValidationContext());
		}

		[Test]
		public void Test_Invoke_succeeds_for_Type_with_registered_factories()
		{
			MockValidatorFactory factory = new MockValidatorFactory();
			Assert.IsFalse(factory.Fail);
			typeof(Dummy).ReplaceValidation(new[] {factory});
			IValidator<Dummy> validator = new ValidateObject().Create<Dummy>();

			validator.Invoke(new Dummy(), new MockValidationContext());
		}

		[Test]
		public void Test_Invoke_fails_for_Type_with_registered_factories()
		{
			MockValidatorFactory factory = new MockValidatorFactory {Fail = true};
			typeof(Dummy).ReplaceValidation(new[] {factory});
			IValidator<Dummy> validator = new ValidateObject().Create<Dummy>();

			factory.Fail = true;

			object value = new Dummy();
			try
			{
				validator.Invoke(value, null);
			}
			catch (ValidationContextException e)
			{
				e.Context.TestValues(value, typeof(Dummy), null, null, null, null);

				ValidationException[] exceptions = e.Exceptions.ToArray();
				Assert.AreEqual(2, exceptions.Count());

				ValidationException exception = exceptions[0];
				exception.Context.TestValues(value, typeof(Dummy), null, null, null, null);
				Assert.AreEqual(typeof(ValidateObjectException), exception.GetType());

				exception = exceptions[1];
				exception.Context.TestValues(value, typeof(Dummy), null, null, null, null);
				Assert.AreEqual("failed", exception.Message);
				return;
			}

			Assert.Fail("ValidationContextException not thrown,");
		}

		[Test]
		public void Test_Invoke_also_invokes_validors_for_inherited_classes_and_interfaces()
		{
			var factoryDummy = new MockValidatorFactoryT<Dummy> {Fail = true};
			var factoryBaseB = new MockValidatorFactoryT<BaseB> {Fail = true};
			var factoryServiceB = new MockValidatorFactoryT<IServiceB> {Fail = true};
			var factoryBaseA = new MockValidatorFactoryT<BaseA> {Fail = true};
			var factoryServiceA = new MockValidatorFactoryT<IServiceA> {Fail = true};

			var factories = new MockValidatorFactory[] {factoryDummy, factoryBaseB, factoryServiceB, factoryBaseA, factoryServiceA};

			typeof(Dummy).ReplaceValidation(new[] {factoryDummy});
			typeof(BaseB).ReplaceValidation(new[] {factoryBaseB});
			typeof(IServiceB).ReplaceValidation(new[] {factoryServiceB});
			typeof(BaseA).ReplaceValidation(new[] {factoryBaseA});
			typeof(IServiceA).ReplaceValidation(new[] {factoryServiceA});

			Dummy value = new Dummy();

			try
			{
				new ValidateObject().Create<Dummy>().Invoke(value, null);
			}
			catch (ValidationContextException e)
			{
				Assert.AreEqual(6, e.Exceptions.Count());
				Assert.AreEqual(typeof(ValidateObjectException), e.Exceptions.First().GetType());
				Assert.IsTrue(factories.FirstOrDefault(f => f.Value != value) == null);
				foreach (var factory in factories)
				{
					Assert.IsNotNull(factory.Context);
				}
				factoryDummy.Context.TestValues(value, typeof(Dummy), null, null, null, null);
				factoryBaseB.Context.TestValues(value, typeof(BaseB), null, null, null, null);
				factoryServiceB.Context.TestValues(value, typeof(IServiceB), null, null, null, null);
				factoryBaseA.Context.TestValues(value, typeof(BaseA), null, null, null, null);
				factoryServiceA.Context.TestValues(value, typeof(IServiceA), null, null, null, null);
				return;
			}
			Assert.Fail("ValidateObjectException not thrown.");
		}

		[Test]
		public void Test_Invoke_fails_for_StateValidationMode_for_invalid_properties()
		{
			ValidationSettings.InterceptValidateProperties = false;

			IValidator<Dummy> validator = new ValidateObject().Create<Dummy>();

			PropertyInfo textProperty = TypeOf<Dummy>.GetProperty(x => x.Text);
			PropertyInfo numberProperty = TypeOf<Dummy>.GetProperty(x => x.Number);

			IValidationContext textPropertyContext = null;
			IValidationContext numberPropertyContext = null;
			textProperty.ReplaceValidation<string>((v, c) =>
			                                       {
			                                       	textPropertyContext = c;
			                                       	throw new ValidationException("property Text failed validation");
			                                       });
			numberProperty.ReplaceValidation<double>((v, c) =>
			                                         {
			                                         	numberPropertyContext = c;
			                                         	throw new ValidationException("property Number failed validation");
			                                         });

			Dummy value = new Dummy {Text = "text", Number = 123};

			try
			{
				validator.Invoke(value, null);
			}
			catch (ValidationContextException e)
			{
				MethodInfo textMethod = textProperty.GetSetMethod();
				ParameterInfo textParameter = textMethod.GetParameters()[0];
				object[] textArgs = new object[] {"text"};
				textPropertyContext.TestValues(value, typeof(Dummy), textProperty, textMethod, textParameter, textArgs);

				MethodInfo numberMethod = numberProperty.GetSetMethod();
				ParameterInfo numberParameter = numberMethod.GetParameters()[0];
				object[] numberArgs = new object[] {123};
				numberPropertyContext.TestValues(value, typeof(Dummy), numberProperty, numberMethod, numberParameter, numberArgs);

				e.Context.TestValues(value, typeof(Dummy), null, null, null, null);

				ValidationException[] exceptions = e.Exceptions.ToArray();
				Assert.AreEqual(3, exceptions.Count());

				ValidationException exception = exceptions[0];
				exception.Context.TestValues(value, typeof(Dummy), null, null, null, null);
				Assert.AreEqual(typeof(ValidateObjectException), exception.GetType());

				exception = exceptions.First(ex => ex.Message == "property Text failed validation");
				exception.Context.TestValues(value, typeof(Dummy), textProperty, textMethod, textParameter, textArgs);

				exception = exceptions.First(ex => ex.Message == "property Number failed validation");
				exception.Context.TestValues(value, typeof(Dummy), numberProperty, numberMethod, numberParameter, numberArgs);
				return;
			}
		}

		[Test]
		public void Test_Invoke_succeeds_for_non_StateValidationMode_for_invalid_properties()
		{
			ValidationSettings.StateValidateProperties = false;

			IValidator<Dummy> validator = new ValidateObject().Create<Dummy>();

			TypeOf<Dummy>.GetProperty(x => x.Text).ReplaceValidation<string>((v, c) => { throw new ValidationException("property Text failed validation"); });
			TypeOf<Dummy>.GetProperty(x => x.Number).ReplaceValidation<double>((v, c) => { throw new ValidationException("property Number failed validation"); });

			validator.Invoke(new Dummy(), new MockValidationContext());
		}

		#region Mocks
		public class Dummy : BaseB, IServiceB
		{
			public string Text { get; set; }
			public double Number { get; set; }
		}

		public interface IServiceA {}

		public interface IServiceB {}

		public class BaseB : BaseA, IServiceA {}

		public abstract class BaseA {}


		public class MockValidatorFactory : IValidatorFactory
		{
			public bool Fail { get; set; }
			public object Value { get; set; }
			public IValidationContext Context { get; set; }

			#region Implementation of IValidatorFactory
			public IValidator<T> Create<T>()
			{
				return new Validator<T>((v, c) =>
				                        {
				                        	Value = v;
				                        	Context = c;
				                        	if (Fail)
				                        		throw new ValidationException("failed");
				                        });
			}
			#endregion
		}

		// ReSharper disable UnusedTypeParameter
		public class MockValidatorFactoryT<TClass> : MockValidatorFactory {}

		// ReSharper restore UnusedTypeParameter
		#endregion
	}
}